Skip to content

Release 0.15.0#516

Draft
ebfull wants to merge 70 commits into
mainfrom
feat/ironwood
Draft

Release 0.15.0#516
ebfull wants to merge 70 commits into
mainfrom
feat/ironwood

Conversation

@ebfull

@ebfull ebfull commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

❗ Do not merge into main until 0.15.0 is released on crates.io and the commit is tagged as 0.15.0.

This is the release PR for 0.15.0, which features NU6.3-related changes including the new circuit and pool APIs for Ironwood. This tracks the feat/ironwood branch, which is currently open for PRs.

We are doing pre-release testing first. We'll be releasing 0.15.0-pre.N from this branch until ZIPs, specs, audits and other internal reviews confirm we're ready for an orchard crate release.

ebfull and others added 30 commits June 13, 2026 14:43
Fix the `BuildError::OutputsDisabled` `Display` string, which described
spends rather than outputs, and tighten related builder, PCZT signer,
and tree doc comments.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename `enable_spends`/`enable_outputs` to `enable_spend`/`enable_output`
throughout the circuit gate, witness assignments, and constraint
comments, matching the per-action semantics of the flags.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `Circuit::empty` constructor for shape-only uses (key generation,
layout rendering) where witnesses are unknown. Move `configure` onto
`Config` and split synthesis into a reusable `synthesize_base`, so the
per-version constraints can build on a shared base.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pull the closure body of `bundle_for_version` into a
`finish_unauthorized_bundle` helper that derives the binding signing
key, builds the actions, checks bsk/bvk consistency, and assembles the
unauthorized bundle.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `single_leaf_witness`, the `SHIELDING` bundle type, and an
`output_only_builder` helper, and rewrite the existing tests to use them
instead of repeating tree-construction and builder boilerplate.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`BatchValidator` now binds its verifying key at construction:
`BatchValidator::new` takes a `&VerifyingKey` and `validate` no longer
does. Removes the `Default` impl, since a verifying key is now required.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Circuit-building APIs (`ProvingKey`/`VerifyingKey::build`,
`Circuit::from_action_context`) now take an explicit `OrchardCircuitVersion`
instead of defaulting to `FixedPostNu6_2`. Removes the temporary `_for_version`
key/circuit APIs and the `Default` impls for `Circuit` and
`OrchardCircuitVersion`; callers must choose a version explicitly.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce `orchard::bundle::BundleProtocol`, the `(pool, era)` selector that
bundle construction is chosen by. It currently projects to the Action circuit
version via `circuit_version()`, with variants `OrchardPreNu6_2` and
`OrchardPreNu6_3`. `orchard::builder::Builder::new` and `orchard::builder::bundle`
take a `BundleProtocol` in place of the removed `Builder::new_for_version` /
`bundle_for_version`, and `Builder::build` derives the circuit version from it.
Proving and verifying keys still take an `OrchardCircuitVersion`, since a key is
a circuit artifact, and the version is threaded that way through the builder
internals.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `Flags::cross_address_enabled` to expose the cross-address bit while
keeping the pre-NU6.3 wire encoding unchanged.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce the NU6.3 flag-byte format, where bit 2 is `enableCrossAddress`,
alongside the pre-NU6.3 format where it is reserved. The crate-private
`BundleFormat` captures this era distinction, and `BundleProtocol` gains the
`OrchardPostNu6_3` and `IronwoodPostNu6_3` variants, projecting to a
`BundleFormat` via `bundle_format()`.

`Flags::{to,from}_byte`, `Bundle::commitment`, and `pczt::Bundle::parse` now
take a `BundleProtocol` and interpret bit 2 according to its era; the same byte
parses as an unrestricted bundle before NU6.3 and a restricted one under NU6.3.
`Flags` gains `CROSS_ADDRESS_DISABLED` (representable only under NU6.3), and
`to_byte`/`commitment` refuse to encode it under a pre-NU6.3 protocol.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `disableCrossAddress` as a circuit public input and reject
restricted bundles for keys that cannot enforce it.
`BatchValidator::add_bundle` now returns `Result` to signal unsupported
restricted bundles.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an `Address` helper that compares derived expanded receiver pairs,
for use by the restricted cross-address checks added in later commits.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the post-NU 6.3 circuit through key and circuit-version selection
ahead of adding its constraints.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ircuit

Add the post-NU 6.3 constraints, the regenerated circuit description, and
proof fixtures for both unrestricted and restricted statements. Pre-NU 6.3
keys continue to reject restricted proof instances.

Surface the consensus constraint as
`BundleProtocol::requires_cross_address_restriction` (true only for
`OrchardPostNu6_3`), and restructure `BundleType::Transactional` to carry
`{ spends_enabled, outputs_enabled, bundle_required }`. The builder derives the
`enableCrossAddress` bit from the protocol — the least-restrictive value
consensus permits — via `BundleType::{flags, num_actions}`, which now take the
`BundleProtocol`.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reject restricted PCZTs with mismatched expanded receivers before
signing, finalizing, or proving them.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pair spends and outputs by expanded receiver, fabricating zero-value
counterparts as needed and rejecting impossible flag combinations.

The fabricated zero-value output paired with each real spend is addressed to the
spent note's own receiver, so its `enc_ciphertext` is randomized rather than
encrypted to that address. A real ciphertext there would trial-decrypt under the
receiver's incoming viewing key in the same action that carries the spend's
nullifier, letting anyone who holds the ivk -- including a quantum adversary who
recovered it from the published address, while nk stays secret -- detect the
spend, collapsing the protocol's asymmetry that hides spends but not receives.
`OutputInfo::fabricated_for_spend` sets a `randomized_ciphertext` flag; `build`
then fills `enc_ciphertext` with random bytes (asserting the value is zero, so it
can never stand in for a value-bearing note) while keeping the genuine epk and
random out_ciphertext, leaving the note and its commitment unchanged so the
circuit's same-expanded-receiver constraint and PCZT note-commitment checks still
hold. Padding and `OutputInfo::dummy` outputs go to throwaway addresses and keep
ordinary encryption.

In a PCZT the output still carries its explicit recipient, value, and rseed with
no user_address, so a signer reads the zero value and classifies it as a dummy it
tolerates -- rather than reconstructing the expected ciphertext and rejecting the
mismatch.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Support restricted builder and PCZT flows end to end, including
fabricated spend signing, padding dummies, and coinbase handling.

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the CHANGELOG preamble summarizing the NU6.3 `enableCrossAddress` flag and
the post-NU 6.3 Orchard Action circuit, and how existing callers preserve the
current behavior by selecting `BundleProtocol::OrchardPreNu6_3` (and
`OrchardCircuitVersion::FixedPostNu6_2` when building proving/verifying keys).

Co-authored-by: GPT 5.5 <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iants

Behaviour-preserving rename of the bundle pool restrictions enum, its
arms, and the `protocol` field/parameters that carry it, to better reflect
what the value actually pins (the information that affects pool-related
restrictions on a bundle), with no logic changes:

- BundleProtocol     -> BundlePoolRestrictions
- OrchardPreNu6_3     -> OrchardNu6_2Only
- OrchardPostNu6_3    -> OrchardNu6_3Onward
- IronwoodPostNu6_3   -> IronwoodNu6_3Onward
- `protocol` field/parameters -> `pool_restrictions`

Also reframes the closely-related doc comments (prose `protocol` ->
pool restrictions / `BundlePoolRestrictions`).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
Carry the underlying VerifyError inside
ProverError::DisallowedCrossAddressTransfer rather than having a unit
variant alongside a separate ProverError::CrossAddressRestriction(VerifyError);
the prover now surfaces the cross-address verification failure through a
single variant. Also reword the VerifyError cross-address Display message
in terms of the bundle's pool restriction.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
… exceptional

Bit 2 of the Orchard flag byte (`enableCrossAddress`) is only representable
for the Ironwood pool post-NU6.3. It is rejected pre-NU6.3 (where bit 2 is
reserved) and for Orchard post-NU6.3 (where consensus mandates the
cross-address restriction). This is now keyed directly on
`BundlePoolRestrictions` in `Flags::{from_byte, to_byte}` rather than via an
intermediate era projection, so the now-redundant `BundleFormat` enum and
`BundlePoolRestrictions::bundle_format` are removed.

`Flags::from_parts` is consolidated to the 3-argument form taking the
cross-address bit; the 2-argument `from_parts_with_cross_address` wrapper is
removed.

Coinbase bundles are not exceptional with respect to the cross-address
restriction: they always disable spends and enable outputs, but set
`enableCrossAddress` exactly as a non-coinbase transaction would for the same
pool restrictions. The `BundleType::Coinbase` flags arm now uses
`default_cross_address_enabled(pool_restrictions)` accordingly.

Tests covering cross-address-enabled flag bytes move to the Ironwood pool
(the only pool that can represent them), and `arb_flags_nu6_3` is renamed to
`arb_flags_ironwood_post_nu6_3`.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
NU6.3 Circuit Implementation
ebfull and others added 19 commits June 24, 2026 15:12
Co-Authored-By: Tal Derei <talderei99@gmail.com>
Co-Authored-By: Tal Derei <talderei99@gmail.com>
Co-Authored-By: Tal Derei <talderei99@gmail.com>
Co-Authored-By: Tal Derei <talderei99@gmail.com>
Co-Authored-By: Dev Ojha <dojha@berkeley.edu>
Replace the internal AnchorCommitment enum with two boolean predicates
(includes_anchor_in_txid_digest / includes_anchor_in_authorizing_digest)
and reword the bundle commitment documentation for clarity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add in v6 and Ironwood TX-id / Auth-ID code
Ironwood: CHANGELOG.md reorganizations and improvements
Previously `commitment_format` ignored `tx_version` for the Ironwood
pool, silently committing an Ironwood bundle under v6 personalization
even when the caller requested `TxVersion::V5`. Add a
`CommitmentError::InvalidTransactionVersion` variant and return it from
the commitment APIs instead.

This makes `authorizing_commitment` and the `hash_bundle_*_empty`
helpers fallible, mirroring `commitment`, and threads
`CommitmentError` through the internal `hash_bundle_*_data` helpers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reject Ironwood commitments for v5 transactions
@nuttycom nuttycom marked this pull request as draft June 29, 2026 16:27
@nuttycom

Copy link
Copy Markdown
Contributor

Converted to draft until we're through the pre-release phase.

nuttycom and others added 5 commits June 29, 2026 13:05
Replace `BundlePoolRestrictions` with `BundleVersion`, the `(ValuePool,
ProtocolVersion)` of an Orchard bundle, built from the new top-level `ValuePool`
and `ProtocolVersion` types via safe-by-construction `const fn` constructors
(`orchard_insecure_v0`, `orchard_v1`, `orchard_v2`, `ironwood_v2`).

Each `Bundle` now carries its `BundleVersion` as non-serialized context, so a
bundle is encodable and committable by construction:

- Construction (`Bundle::from_parts` / `try_from_parts`, the builder, and PCZT
  parsing/extraction) takes a `BundleVersion` and validates that the flags are
  representable under it, rejecting inconsistent combinations with the new
  `BundleError::UnrepresentableFlags`. `from_parts` is now fallible.
- `commitment` / `authorizing_commitment` and the `decrypt_*` / `recover_*`
  helpers no longer take a version argument; they read it from the bundle.
  `Bundle::flag_byte` exposes the now-infallible flag-byte encoding and
  `Bundle::bundle_version` the carried version.
- `CommitmentError::UnrepresentableFlags` is removed (flags are validated at
  construction); only `InvalidTransactionVersion` remains.

Proof-size enforcement is derived from the bundle version rather than a separate
`ProofSizeEnforcement` argument: it is enforced for every version except the
historical pre-NU6.2 Orchard pool (`orchard_insecure_v0`), whose already-committed
transactions may carry non-canonical proofs. `ProofSizeEnforcement` is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`BundleType` previously embedded part of a bundle's `Flags`
(`Transactional` carried `spends_enabled`/`outputs_enabled`, and
`BundleType::flags` derived the cross-address bit from the bundle
version). This conflated two independent concerns: the construction
discipline (how the builder pads and pairs actions) and the bundle's
flag set (which spend/output/cross-address capabilities it advertises).
It also meant the caller could not restrict cross-address transfers more
tightly than the bundle version's default chose to.

Make the two orthogonal. `BundleType` is now just the construction
policy — `Transactional { bundle_required }` or `Coinbase` — and the
bundle's `Flags` are supplied separately to the builder. The default
flag set moves to `BundleVersion::default_flags` (spends and outputs
enabled, cross-address transfers enabled unless the version mandates the
restriction); a caller may pass a more restricted set the version
permits.

Validate the flags when the builder is constructed: `Builder::new` (and
the free `bundle` function) now take `Flags` and are fallible, rejecting
a flag set that cannot be encoded under the bundle version with
`BuildError::UnrepresentableFlags`, and rejecting a `Coinbase` builder
whose flags enable spends with `BuildError::CoinbaseSpendsEnabled`.
Because the flags are validated up front, `build_bundle` can assume they
are encodable (a `debug_assert!` documents the invariant).
`BundleType::num_actions` now reads the spend/output/cross-address
policy from the supplied `Flags` rather than re-deriving it from the
bundle version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `try_from_parts` doc linked to the `pub(crate)`
`BundleVersion::enforces_canonical_proof_size`, which trips
`rustdoc::private_intra_doc_links` (an error under `-D warnings`). Point at the
public `BundleVersion::orchard_insecure_v0` constructor instead: it builds the
sole bundle version whose proof size is not enforced, so it is the more useful
public reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rename the `ProtocolVersion` variants and `BundleVersion` constructors from
0-based to 1-based version numbers, at the protocol developers' request:

  ProtocolVersion::InsecureV0 -> InsecureV1
  ProtocolVersion::V1         -> V2
  ProtocolVersion::V2         -> V3

  BundleVersion::orchard_insecure_v0 -> orchard_insecure_v1
  BundleVersion::orchard_v1          -> orchard_v2
  BundleVersion::orchard_v2          -> orchard_v3
  BundleVersion::ironwood_v2         -> ironwood_v3

This is a pure rename with no change in behavior; each version keeps the same
mapping to consensus epochs, circuit versions, and note plaintext versions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reorder the free `bundle` function to take `bundle_type` immediately after
`rng`, matching `Builder::new`'s `(bundle_type, bundle_version, flags, anchor)`
order. Also note the removal of `BundleType::DISABLED` in the changelog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ebfull added 2 commits June 29, 2026 17:22
Distinguish value pool, protocol version, and bundle version
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants